dot+
for macOS
The iOS app is the source of truth. macOS is a menu-bar companion that reuses the same design system and services and re-homes the UI for the desktop: a status-bar item, a detachable popover for capture and review, and an optional full window for the library. This page is what you build against.
UIColor, share sheet) for AppKit equivalents. The map below marks exactly what reuses, adapts, or is new.The interactive reference
The clickable mockup below is the behavioural spec. Every menu-bar state is modelled — see it work, then build it. Use the Tweaks panel (top-right of the frame) to jump between states and try theme, accent, panel width, and layout variants.
Platform translation
How each iOS pattern re-homes on the desktop. The design language does not change — the container and the input model do. Touch becomes pointer; the tab bar becomes structure; the Dynamic Island becomes the status item.
NavigationSplitView. The 4 tabs become a segmented control or sidebar..sheet sized to content — no detents on macOS.UNUserNotification in Notification Center.NSHapticFeedbackManager (trackpad only, sparingly). Keep the visual state change.NavigationStack works. In the popover keep depth shallow — prefer a back affordance over deep stacks..contextMenu. Refresh is a toolbar button, not a pull.ShareLink or NSSharingServicePicker. Export to file uses .fileExporter..windowStyle(.hiddenTitleBar)) with in-content controls.Design tokens
Copy-paste ready. The only macOS change from the iOS token file: UIColor(dynamicProvider:) becomes NSColor(name:dynamicProvider:). Values are identical — tokens.json is the law. Never reference primitives from views; always go through semantic.
import SwiftUI
// Theme-adaptive color from explicit dark/light primitives.
// macOS: wrap NSColor(name:dynamicProvider:) so SwiftUI
// updates with the effective appearance.
extension Color {
static func themed(dark: Color, light: Color) -> Color {
Color(NSColor(name: nil) { appearance in
let isDark = appearance.bestMatch(
from: [.aqua, .darkAqua]) == .darkAqua
return isDark ? NSColor(dark) : NSColor(light)
})
}
// Surfaces
static let surfacePrimary = themed(dark: .mono900, light: .mono0)
static let surfaceSecondary = themed(dark: .mono800, light: .mono50)
static let surfaceTertiary = themed(dark: .mono700, light: .mono100)
static let surfaceInverse = themed(dark: .mono50, light: .mono1000)
// Content
static let contentPrimary = themed(dark: .mono0, light: .mono1000)
static let contentSecondary = themed(dark: .mono300, light: .mono600)
static let contentTertiary = themed(dark: .mono500, light: .mono500)
// Border
static let borderOpaque = themed(dark: .mono600, light: .mono200)
static let borderSubtle = themed(dark: .mono700, light: .mono100)
// State (theme-invariant)
static let statePositive = Color(hex: 0x05944F)
static let stateWarning = Color(hex: 0xBB7A00)
static let stateNegative = Color(hex: 0xE11900)
static let stateInfo = Color(hex: 0x276EF1)
}
/* primitive — mono */
:root {
--mono-0:#FFFFFF; --mono-50:#FAFAFC;
--mono-100:#F2F2F5; --mono-200:#E6E6EB;
--mono-300:#C8C8D4; --mono-400:#9494A3;
--mono-500:#6B6B78; --mono-600:#2E2E38;
--mono-700:#222229; --mono-800:#18181F;
--mono-900:#0F0F14; --mono-1000:#05050A;
/* accents */
--positive:#05944F; --warning:#BB7A00;
--negative:#E11900; --info:#276EF1;
/* semantic — dark (primary) */
--bg:var(--mono-900); --bg-2:var(--mono-800);
--bg-3:var(--mono-700); --bg-4:var(--mono-600);
--fg:var(--mono-0); --fg-2:var(--mono-300);
--fg-3:var(--mono-400); --line:var(--mono-700);
}
/* light — one-swap override */
:root[data-theme=light] {
--bg:var(--mono-0); --bg-2:var(--mono-50);
--bg-3:var(--mono-100); --fg:var(--mono-1000);
--fg-2:var(--mono-600); --line:var(--mono-100);
}
enum DotSpacing { // 4pt grid
static let s1: CGFloat = 4 // icon-label gap
static let s2: CGFloat = 8 // within-component
static let s3: CGFloat = 12 // card / button pad
static let s4: CGFloat = 16 // edge, between cards
static let s5: CGFloat = 20 // section spacing
static let s6: CGFloat = 24 // major break
static let s7: CGFloat = 32 // above headers
static let s8: CGFloat = 48
}
enum DotRadius { // never > 16
static let xs: CGFloat = 4 // tags, badges
static let sm: CGFloat = 8 // cards, buttons
static let md: CGFloat = 12
static let lg: CGFloat = 16 // ceiling
}
extension Font { // Inter + JetBrains Mono
static let dotDisplay = custom("Inter-Bold", size: 32)
static let dotHeadline = custom("Inter-Bold", size: 22)
static let dotTitle = custom("Inter-SemiBold", size: 18)
static let dotBody = custom("Inter-Regular", size: 15)
static let dotBodyStrong = custom("Inter-SemiBold", size: 15)
static let dotLabel = custom("Inter-Medium", size: 13)
static let dotCaption = custom("JetBrainsMono-Regular", size: 11)
}
// Register fonts in the app target's Info.plist
// under ATSApplicationFontsPath (macOS) — not UIAppFonts.
UIAppFonts. On macOS use ATSApplicationFontsPath in Info.plist (point it at a Fonts/ folder in the bundle), or register at launch with CTFontManagerRegisterFontsForURL. If a face is missing, the tokens fall back to SF Pro / SF Mono automatically.Components → SwiftUI
The core components are cross-platform Swift. They render as-is; the only edits are pointer affordances (hover, focus ring) and removing the UIKit haptic call. Each card: the live look, the contract, and the macOS note.
UIImpactFeedbackGenerator. Add .onHover brightness lift + .focusable() ring. Keep the 0.97 press scale.radius.xs, caption type. Semantic variants use a 16% tinted fill of the state color, no border.
Switch style.@FocusState, not just tap.DotButton("Record Meeting", icon: "mic.fill") { start() }
DotButton("Upload Audio", variant: .secondary) { pick() }
DotButton("Cancel", variant: .tertiary, size: .small) { dismiss() }
DotButton("", icon: "mic.fill", shape: .circle) { start() }
UIImpactFeedbackGenerator) behind #if os(iOS) inside the component. Every call site stays the same.Screen build checklist
Every surface the macOS app needs, its iOS source, and the porting note. All are designed in the mockup and not yet built in the macOS target — check them off as you land each one.
mm:ss timer. Click toggles the popover.AuthenticationManager reused.VoiceChatServices reused.Settings scene (⌘,) as a tabbed window, plus quick toggles in the popover.NavigationSplitView — sidebar list, detail on the trailing side. Resizable.UNNotificationAction.StoreKitService reused. Present as a window sheet. Verify macOS IAP entitlement.Architecture map
The Xcode project, grouped. The service and token layers are platform-agnostic Swift — carry them over. The view layer is where the work is. Tags mark what reuses as-is, what needs a macOS adaptation, and what's net-new.
DesignSystem
Services
Features (views)
Managers · App shell
MenuBarExtra shell + status item. ③ Port Home → Recording → Detail (the core loop). ④ Full window + Template Store grid. ⑤ Settings scene, notifications, paywall. Land the core capture loop before the library.